Spring笔记(七)—— AOP 之 @AspectJ 支持

开启 @AspectJ 支持

@AspectJ 指的是使用注解标注的常规 Java 类的样式声明切面,@AspectJ 样式是 AspectJ 项目引入的,作为 AspectJ 5 版本的一部分。Spring 使用由 AspectJ 提供的库 aspectjweaver.jar(要求版本至少为 1.6.8),就能像 AspectJ 5 那样解析注解。

Java 配置方式

1
2
3
4
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

XML 配置方式

1
<aop:aspectj-autoproxy/>

声明切面(aspect)

当开启 @AspectJ 支持时,任何在应用程序上下文中使用类(包含 @AspectJ 注解)定义的 bean 都会被 Spring 侦测到,并用于配置 Spring AOP。
如下为应用程序上下文中的常规 bean 定义,指向具有 @AspectJ 注解的 bean 类:

1
2
3
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>

还有具有 @AspectJ 注解的 NotVeryUsefulAspect 类:

1
2
3
4
5
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}

声明切点(pointcut)

切点能够定位特定的连接点,从而使我们能够控制何时执行增强操作。切点声明有两部分:一个包含方法名和任意参数的签名,以及一个切入点表达式(它标明要执行的方法的位置)。在 AOP 的 @AspectJ 注解风格中,切点签名由一个常规方法定义,切点表达式使用 @Pointcut 注解表示(用作切点签名的方法必须具有 void 返回类型)。

1
2
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

切点表达式函数

Aspectj5 的切点表达式由关键字和操作参数组成,如 execution(* transfer(..))execution 为关键字,* transfer(..) 为操作参数。在上面的例子中,execution 代表目标类执行某一方法,* transfer(..) 描述目标方法的匹配模式串,两个联合起来表示目标类 transfer() 方法的连接点。

Spring 支持 9 个 @AspectJ 切点表达式函数,它们用不同的方式描述目标类的连接点,根据描述对象的不同,大致分为 4 个类型:

  • 方法切点函数:通过描述目标类方法信息定义连接点。
    • execution(方法匹配模式串):表示满足某一匹配模式的所有目标方法连接点。如 execution(* transfer(..)) 表示所有目标类中的 transfer() 方法;execution(public * *(..)) 表示所有目标类中的 public 方法。
    • @annotation(方法注解类名):表示标注了特定注解的目标方法连接点。如 @annotation(com.lake.NeedTest) 表示任何标注了 @NeedTest 注解的目标类方法。
  • 方法入参切点函数:通过描述目标类方法入参的信息定义连接点。
    • args(类名):通过判别目标类方法运行时入参对象的类型定义指定连接点。如 args(com.lake.Waiter) 表示所有有且仅有一个按类型匹配于 Waiter 入参的方法。
    • @args(类型注解类名):通过判别目标方法运行时入参对象的类是否标注特定注解来指定连接点。如 @args(com.lake.Monitorable) 表示任何这样的一个目标方法:它有一个入参且入参对象的类标注 @Monitorable 注解。
  • 目标类切点函数:通过描述目标类类型信息定义连接点。
    • within(类名匹配串):表示特定域下的所有连接点。如 within(com.lake.service.*) 表示 com.lake.service 包中的所有连接点,即包中所有类的所有方法,而 within(com.lake.service.*Service) 表示在 com.lake.service 包中所有以 Service 结尾的类的所有连接点。
    • target(类名):假如目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如通过 target(com.lake.Waiter) 定义的切点、Waiter 以及 Waiter 实现类 NativeWaiter 中所有连接点都匹配该切点。
    • @within(类型注解类名):假如目标类按类型匹配于某个类 A,且类 A 标注了特定注解,则目标类的所有连接点匹配这个切点。如 @within(com.lake.Monitorable) 定义的切点,假如 Waiter 类标注了 @Monitorable 注解,则 Waiter 以及Waiter 实现类 NativeWaiter 中所有连接点都匹配。
    • @target(类型注解类名):目标类标注了特定注解,则目标类所有连接点匹配这个切点。如 @target(com.lake.Monitorable) 定义的切点,假如 NativeWaiter 类标注了 @Monitorable 注解,则 NativeWaiter 所有连接点都匹配切点。
  • 代理类切点函数:通过描述目标类的代理类的信息定义连接点。
    • this(类名):代理类按类型匹配于指定类,则被代理的目标类所有连接点匹配切点。

函数入参通配符

@AspectJ 支持 3 种通配符:

  • * :匹配任意字符,但它只能匹配上下文中的一个元素
  • .. :匹配任意字符,可以匹配上下文中的多个元素,但在表示类时,必须和 * 联合使用,而在表示入参时则单独使用
  • + :表示按类型匹配指定类的所有类,必须跟在类名后面,如 com.lake.Car+。继承或扩展指定类的所有类,同时还包括指定类本身

@AspectJ 函数按其是否支持通配符及支持的程度,分为 3 类:

  • 支持所有通配符:execution(), within()
  • 仅支持 + 通配符:args(), this(), target()
  • 不支持通配符:@args(), @within(), @target(), @annotation()

逻辑运算符

切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点,Spring 支持以下切点运算符:

  • && :与操作符,等效符号 and。如 within(com.lake.*) and args(String) 表示在 com.lake 包下所有类拥有一个 String 入参的方法
  • || :或操作符,等效符号 or。如 within(com.lake..*) and args(String) 表示在 com.lake 包下所有类(当前包以及子孙包)的方法,或所有拥有一个 String 入参的方法
  • ! :非操作符,等效符号 not。如 !within(com.lake.*) 表示所有不在 com.lake 包下的方法

声明增强(advice)

Spring 使用增强定义横切逻辑,同时由于 Spring 只支持方法连接点,增强还包括了在方法的哪一点加入横切代码的方位信息,所以增强既包括横切逻辑,还包括部分连接点。
增强与切点表达式相关联,并在切点匹配的方法执行之前,之后或周围运行。切点表达式可以是对指定切点的简单引用,也可以是在适当位置声明的切入点表达式。所有增强类型除了引介增强,其余都属于方法级别的。

@Before

前置增强表示在目标方法执行前实施增强(即先执行增强方法中的代码)。
使用 @Before 声明前置增强,其拥有两个属性:

  • vlaue:该成员用于定义切点。
  • argNames:由于无法通过 Java 反射机制获取方法入参名,所有如果在 Java 编译时未启动调试信息或者需要在运行期解析切点,就必须通过这个成员指定注解多标注增强方法的参数名(注意两者名字必须完全相同),多个参数名用逗号分隔。
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck2() {
// ...
}
}

如上所示,doAccessCheck() 方法会在 SystemArchitecture 类的 dataAccessOperation() 方法执行之前先执行;doAccessCheck2() 会在表达式所匹配的方法执行之前执行。

@AfterReturning

后置增强表示在目标方法执行后实施增强(即后执行增强方法中的代码)。
使用 @AfterReturning 声明后置增强,其拥有四个属性:

  • vlaue:该成员用于定义切点。
  • pointcut:表示切点的信息,如果显示指定 pointcut 值,它将覆盖 value 的设置值,可以将 pointcut 属性看成 value。
  • returning:将目标对象方法的返回值绑定给增强的方法。
  • argNames:如上所述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck2(Object retVal) {
// ...
}
}

@Around

环绕增强表示在目标方法执行前后实施增强(即前后都执行增强方法中的代码)。
使用 @Around 声明环绕增强,其拥有两个属性:

  • vlaue:该成员用于定义切点。
  • argNames:如上所述
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}

@AfterThrowing

异常抛出增强表示在目标方法抛出异常后实施增强。
使用 @AfterThrowing 声明抛出增强,其拥有四个属性:

  • vlaue:该成员用于定义切点。
  • pointcut:表示切点的信息,如果显示指定 pointcut 值,它将覆盖 value 的设置值,可以将 pointcut 属性看成 value。
  • returning:将抛出的异常绑定给增强的方法。
  • argNames:如上所述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}

@After

Final 增强,不管是抛出异常或者是正常退出,该增强都会得到执行,一般用于释放资源,相当于 finally。
使用 @After 声明 Final 增强,其拥有两个属性:

  • vlaue:该成员用于定义切点。
  • argNames:如上所述
1
2
3
4
5
6
7
8
9
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}

@DeclareParents

引介增强是一种比较特殊的增强类型,它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的。通过引介增强可以为目标类添加一个接口的实现,即原来目标类未实现某个接口,通过引介增强可以为目标类创建实现某接口的代理。
使用 @DeclareParents 声明引介增强,其拥有两个属性:

  • vlaue:该成员用于定义切点,它表示在哪个目标类上添加引介增强。
  • defaultImpl:默认的接口实现类。

示例:给定一个接口 UsageTracked 以及该接口的实现 DefaultUsageTracked,vlaue 属性指向需要添加接口实现的类,defaultImpl 属性指向默认的接口实现类。通过 @DeclareParents 为 value 属性的 AspectJ 切点表达式语法所匹配的类添加一个需要实现的 UsageTracked 接口,并指定其默认实现类为 DefaultUsageTracked,然后通过切面技术将 DefaultUsageTracked 融合到 value 属性的 AspectJ 切点表达式语法所匹配的类,这样 value 属性的 AspectJ 切点表达式语法所匹配的类就实现了 UsageTracked 接口了。

1
2
3
4
5
6
7
8
9
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}

1
interface UsageTracked {}

参考资料:

Spring 3.x 企业应用开发实战
Spring Framework Reference Documentation

热评文章